// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.idea.maven.dsl

import com.intellij.buildsystem.model.unified.UnifiedCoordinates
import com.intellij.buildsystem.model.unified.UnifiedDependency
import com.intellij.buildsystem.model.unified.UnifiedDependencyRepository
import com.intellij.externalSystem.ExternalDependencyModificator
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.xml.XmlFile
import com.intellij.psi.xml.XmlTag
import com.intellij.util.xml.DomUtil
import com.intellij.util.xml.GenericDomValue
import org.jetbrains.annotations.NotNull
import org.jetbrains.idea.maven.dom.MavenDomElement
import org.jetbrains.idea.maven.dom.MavenDomUtil
import org.jetbrains.idea.maven.dom.converters.MavenDependencyCompletionUtil
import org.jetbrains.idea.maven.dom.model.MavenDomDependency
import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel
import org.jetbrains.idea.maven.dom.model.MavenDomRepository
import org.jetbrains.idea.maven.model.MavenConstants.SCOPE_COMPILE
import org.jetbrains.idea.maven.model.MavenId
import org.jetbrains.idea.maven.project.MavenProject
import org.jetbrains.idea.maven.project.MavenProjectBundle
import org.jetbrains.idea.maven.project.MavenProjectsManager

val mavenTopLevelElementsOrder = listOf(
  "modelVersion",
  "parent",
  "groupId",
  "artifactId",
  "version",
  "packaging",
  "properties",

  "name",
  "description",
  "url",
  "inceptionYear",
  "licenses",
  "organization",
  "developers",
  "contributors",

  "modules",
  "dependencyManagement",
  "dependencies",
  "build",
  "reporting",

  "issueManagement",
  "ciManagement",
  "mailingLists",
  "scm",
  "prerequisites",
  "repositories",
  "pluginRepositories",
  "distributionManagement",
  "profiles"
)

val elementsBeforeDependencies = elementsBefore("dependencies")
val elementsBeforeRepositories = elementsBefore("repositories")

fun elementsBefore(s: String): Set<String> {
  return mavenTopLevelElementsOrder.takeWhile { it != s }
    .toSet()
}

class MavenDependencyModificator(private val myProject: Project) : ExternalDependencyModificator {
  private val myProjectsManager: MavenProjectsManager = MavenProjectsManager.getInstance(myProject)
  private val myDocumentManager: PsiDocumentManager = PsiDocumentManager.getInstance(myProject)


  private fun addDependenciesTagIfNotExists(psiFile: XmlFile, model: MavenDomProjectModel) {
    addTagIfNotExists(psiFile, model.dependencies, elementsBeforeDependencies)
  }

  private fun addRepositoriesTagIfNotExists(psiFile: XmlFile, model: MavenDomProjectModel) {
    addTagIfNotExists(psiFile, model.repositories, elementsBeforeRepositories)
  }

  private fun addTagIfNotExists(psiFile: XmlFile, element: MavenDomElement, elementsBefore: Set<String>) {
    if (element.exists()) {
      return
    }
    val rootTag = psiFile.rootTag;
    if (rootTag == null) {
      element.ensureTagExists();
      return
    }
    val children = rootTag.children
    if (children == null || children.isEmpty()) {
      element.ensureTagExists()
      return
    }
    val lastOrNull = children
      .mapNotNull { it as? XmlTag }
      .lastOrNull { elementsBefore.contains(it.name) }

    val child = rootTag.createChildTag(element.xmlElementName, rootTag.namespace, null, false)
    rootTag.addAfter(child, lastOrNull)
  }

  private fun getDependenciesModel(module: Module,
                                   groupId: String, artifactId: String): Pair<MavenDomProjectModel, MavenDomDependency?> {
    val project: MavenProject = myProjectsManager.findProject(module) ?: throw IllegalArgumentException(MavenProjectBundle.message(
      "maven.project.not.found.for", module.name))

    return ReadAction.compute<Pair<MavenDomProjectModel, MavenDomDependency?>, Throwable> {
      val model = MavenDomUtil.getMavenDomProjectModel(myProject, project.getFile()) ?: throw IllegalStateException(
        MavenProjectBundle.message("maven.model.error", module.name))
      val managedDependency = MavenDependencyCompletionUtil.findManagedDependency(model, myProject, groupId, artifactId)
      return@compute Pair(model, managedDependency)
    }

  }

  override fun supports(module: Module): Boolean {
    return myProjectsManager.isMavenizedModule(module)
  }

  override fun addDependency(module: Module, descriptor: UnifiedDependency) {
    requireNotNull(descriptor.coordinates.groupId)
    requireNotNull(descriptor.coordinates.version)
    requireNotNull(descriptor.coordinates.artifactId)
    val mavenId = descriptor.coordinates.toMavenId()
    val (model, managedDependency) = getDependenciesModel(module, mavenId.groupId!!, mavenId.artifactId!!)

    val psiFile = DomUtil.getFile(model)
    WriteCommandAction.writeCommandAction(myProject, psiFile).compute<Unit, Throwable> {
      addDependenciesTagIfNotExists(psiFile, model);
      val dependency = MavenDomUtil.createDomDependency(model, null)
      dependency.groupId.stringValue = mavenId.groupId
      dependency.artifactId.stringValue = mavenId.artifactId
      val scope = toMavenScope(descriptor.scope, managedDependency?.scope?.stringValue);
      scope?.let { dependency.scope.stringValue = it }
      if (managedDependency == null || managedDependency.version.stringValue != mavenId.version) {
        dependency.version.stringValue = mavenId.version
      }
      saveFile(psiFile)
    }

  }

  override fun updateDependency(module: Module, oldDescriptor: UnifiedDependency, newDescriptor: UnifiedDependency) {
    requireNotNull(newDescriptor.coordinates.groupId)
    requireNotNull(newDescriptor.coordinates.version)
    requireNotNull(newDescriptor.coordinates.artifactId)
    val oldMavenId = oldDescriptor.coordinates.toMavenId()
    val newMavenId = newDescriptor.coordinates.toMavenId()
    val (model, managedDependency) = ReadAction.compute<Pair<MavenDomProjectModel, MavenDomDependency?>, Throwable> {
      getDependenciesModel(module, oldMavenId.groupId!!, oldMavenId.artifactId!!)
    }

    val psiFile = managedDependency?.let(DomUtil::getFile) ?: DomUtil.getFile(model)
    WriteCommandAction.writeCommandAction(myProject, psiFile).compute<Unit, Throwable> {
      if (managedDependency != null) {
        val parentModel = managedDependency.getParentOfType(MavenDomProjectModel::class.java, true)
        if (parentModel != null) {
          updateVariableOrValue(parentModel, managedDependency.version, newMavenId.version!!)
        }
        else {
          managedDependency.version.stringValue = newDescriptor.coordinates.version
        }
      }
      else {
        for (dep in model.dependencies.dependencies) {
          if (dep.artifactId.stringValue == oldMavenId.artifactId && dep.groupId.stringValue == oldMavenId.groupId) {
            updateVariableOrValue(model, dep.artifactId, newMavenId.artifactId!!)
            updateVariableOrValue(model, dep.groupId, newMavenId.groupId!!)
            updateVariableOrValue(model, dep.version, newMavenId.version!!)
          }
        }
      }
      saveFile(psiFile)
    }
  }

  override fun removeDependency(module: Module, descriptor: UnifiedDependency) {
    requireNotNull(descriptor.coordinates.groupId)
    requireNotNull(descriptor.coordinates.version)
    val mavenId = descriptor.coordinates.toMavenId()
    val (model, _) = getDependenciesModel(module, mavenId.groupId!!, mavenId.artifactId!!)

    val psiFile = DomUtil.getFile(model)
    WriteCommandAction.writeCommandAction(myProject, psiFile).compute<Unit, Throwable> {
      for (dep in model.dependencies.dependencies) {
        if (dep.artifactId.stringValue == mavenId.artifactId && dep.groupId.stringValue == mavenId.groupId) {
          dep.xmlTag?.delete()
        }
      }
      if (model.dependencies.dependencies.isEmpty()) {
        model.dependencies.xmlTag?.delete();
      }
      saveFile(psiFile)
    }
  }

  override fun addRepository(module: Module, repository: UnifiedDependencyRepository) {
    val project: MavenProject = myProjectsManager.findProject(module) ?: throw IllegalArgumentException(MavenProjectBundle.message(
      "maven.project.not.found.for", module.name))
    val model = ReadAction.compute<MavenDomProjectModel, Throwable> {
      MavenDomUtil.getMavenDomProjectModel(myProject, project.getFile()) ?: throw IllegalStateException(
        MavenProjectBundle.message("maven.model.error", module.name))
    }
    for (repo in model.repositories.repositories) {
      if (repo.url.stringValue?.trimLastSlash() == repository.url.trimLastSlash()) {
        return;
      }
    }

    val psiFile = DomUtil.getFile(model)
    WriteCommandAction.writeCommandAction(myProject, psiFile).compute<Unit, Throwable> {
      addRepositoriesTagIfNotExists(psiFile, model)
      val repoTag = model.repositories.addRepository()
      repository.id?.let { repoTag.id.stringValue = it }
      repository.name?.let { repoTag.name.stringValue = it }
      repository.url.let { repoTag.url.stringValue = it }
      saveFile(psiFile)
    }
  }

  override fun deleteRepository(module: Module, repository: UnifiedDependencyRepository) {
    val project: MavenProject = myProjectsManager.findProject(module) ?: throw IllegalArgumentException(MavenProjectBundle.message(
      "maven.project.not.found.for", module.name))
    val (model, repo) = ReadAction.compute<Pair<MavenDomProjectModel, MavenDomRepository?>, Throwable> {
      val model = MavenDomUtil.getMavenDomProjectModel(myProject, project.getFile()) ?: throw IllegalStateException(
        MavenProjectBundle.message("maven.model.error", module.name))
      for (repo in model.repositories.repositories) {
        if (repo.url.stringValue?.trimLastSlash() == repository.url.trimLastSlash()) {
          return@compute Pair(model, repo)
        }
      }
      return@compute null;
    }
    if (repo == null) return;
    val psiFile = DomUtil.getFile(repo)
    WriteCommandAction.writeCommandAction(myProject, psiFile).compute<Unit, Throwable> {
      repo.xmlTag?.delete()
      if(model.repositories.repositories.isEmpty()) {
        model.repositories.xmlTag?.delete()
      }
      saveFile(psiFile)
    }

  }

  private fun saveFile(psiFile: @NotNull XmlFile) {
    val document = myDocumentManager.getDocument(psiFile) ?: throw IllegalStateException(MavenProjectBundle.message(
      "maven.model.error", psiFile));
    myDocumentManager.doPostponedOperationsAndUnblockDocument(document)
    FileDocumentManager.getInstance().saveDocument(document)
  }

  private fun updateVariableOrValue(model: MavenDomProjectModel,
                                    domValue: GenericDomValue<String>,
                                    newValue: String) {
    val rawText = domValue.rawText?.trim()
    if (rawText != null && rawText.startsWith("${'$'}{") && rawText.endsWith("}")) {
      val propertyName = rawText.substring(2, rawText.length - 1);
      val subTags = model.properties.xmlTag?.subTags ?: emptyArray()
      for (subTag in subTags) {
        if (subTag.name == propertyName) {
          //TODO: recursive property declaration
          subTag.value.text = newValue
          return
        }
      }
    }
    else {
      domValue.stringValue = newValue
    }
  }


  private fun toMavenScope(scope: String?, managedScope: String?): String? {
    if (managedScope == null) {
      if (scope == null || scope == SCOPE_COMPILE) return null
      return scope;
    }
    if (scope == managedScope) return null
    return scope
  }


  override fun declaredDependencies(module: Module): List<UnifiedDependency> {
    val project = MavenProjectsManager.getInstance(module.project).findProject(module) ?: return emptyList()


    return ReadAction.compute<List<UnifiedDependency>, Throwable> {
      val model = MavenDomUtil.getMavenDomProjectModel(myProject, project.getFile()) ?: throw IllegalStateException(
        MavenProjectBundle.message("maven.model.error", module.name))
      model.dependencies.dependencies.map {
        var scope = it.scope.stringValue;
        if (scope == SCOPE_COMPILE) scope = null
        UnifiedDependency(it.groupId.stringValue, it.artifactId.stringValue, it.version.rawText, scope)
      }
    }

  }

  override fun declaredRepositories(module: Module): List<UnifiedDependencyRepository> {
    val project = MavenProjectsManager.getInstance(module.project).findProject(module) ?: return emptyList()
    return ReadAction.compute<List<UnifiedDependencyRepository>, Throwable> {
      val model = MavenDomUtil.getMavenDomProjectModel(myProject, project.getFile()) ?: throw IllegalStateException(
        MavenProjectBundle.message("maven.model.error", module.name))
      model.repositories.repositories.map {
        UnifiedDependencyRepository(it.id.stringValue, it.name.stringValue, it.url.stringValue ?: "")
      }
    }
  }

}

private fun String.trimLastSlash(): String {
  return trimEnd('/');
}

private fun UnifiedCoordinates.toMavenId(): MavenId {
  return MavenId(groupId, artifactId, version)
}
